﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

using TracedReality.RayTracing;
using QiHe.Yaml.Grammar;

namespace TracedReality.Primitive
{
    /// <summary>
    /// A cylindrical ring.
    /// </summary>
    public class Cylinder : Shape
    {
        /// <summary>
        /// Center of the cylinder.
        /// </summary>
        public Vector center;

        /// <summary>
        /// Height of the cylinder.
        /// </summary>
        public double height;

        /// <summary>
        /// Innder radius of the ring.
        /// </summary>
        public double innerRad;

        /// <summary>
        /// Outer radius of the ring.
        /// </summary>
        public double outerRad;

        /// <summary>
        /// Create a Cylinder with the given center, height, inner radius, outer radius, and Material.
        /// </summary>
        /// <param name="c">Center.</param>
        /// <param name="h">Height.</param>
        /// <param name="ir">Innder radius.</param>
        /// <param name="or">Outer radius.</param>
        /// <param name="m">Material.</param>
        public Cylinder(Vector c, double h, double ir, double or, ref Material m)
        {
            Debug.Assert(ir >= 0);
            Debug.Assert(ir < or);
            Debug.Assert(h > 0);

            center = c;
            height = h;
            innerRad = ir;
            outerRad = or;
            material = m;

            Vector min = new Vector(center);
            min.x -= outerRad;
            min.y -= height / 2;
            min.z -= outerRad;

            Vector max = new Vector(center);
            max.x += outerRad;
            max.y += height / 2;
            max.z += outerRad;

            bound = new BoundingBox(min, max);
        }

        public override bool Equals(Shape s)
        {
            if (s is Cylinder) { return Equals((Cylinder)s); }
            return false;
        }

        public bool Equals(Cylinder c)
        {
            return center.Equals(c.center) &&
                height == c.height &&
                innerRad == c.innerRad &&
                outerRad == c.outerRad &&
                material.Equals(c.material);
        }

        public override bool Similar(Shape s)
        {
            if (!(s is Cylinder)) { return false; }
            Cylinder c = (Cylinder)s;

            return height == c.height &&
                innerRad == c.innerRad &&
                outerRad == c.outerRad &&
                material.Equals(c.material);
        }

        /// <summary>
        /// Used for intersection testing to see if the Ray passes through the a Cylinder of the
        /// specified radius.
        /// </summary>
        /// <param name="ray"></param>
        /// <param name="cen"></param>
        /// <param name="rad"></param>
        /// <param name="he"></param>
        /// <param name="t"></param>
        /// <param name="n"></param>
        /// <returns></returns>
        private static int intersectFiniteCylinder(Ray ray, Vector cen, double rad, double he, ref double t, ref Vector n)
        {
            Vector or = ray.origin;
            Vector di = ray.direction;

            double a = (di.x * di.x) + (di.z * di.z);
            double b = 2 * ((di.x * or.x) + (di.z * or.z));
            double c = (or.x * or.x) + (or.z * or.z) - rad * rad;

            double d = (b * b) - (4 * a * c);
            if (d < Settings.EPSILON) { return 0; }

            d = Math.Sqrt(d);

            double tmin = (-b - d) / (2 * a);
            double tplu = (-b + d) / (2 * a);

            Vector tmp = ray.pointAtParameter(tmin);
            t = tmin;
            if (tmin < Settings.EPSILON ||
                tmp.y > (cen.y + (he / 2.0)) ||
                tmp.y < (cen.y - (he / 2.0)))
            {
                t = tplu;
            }

            Vector point = ray.pointAtParameter(t);
            if (t < Settings.EPSILON ||
                point.y > (cen.y + (he / 2.0)) ||
                point.y < (cen.y - (he / 2.0)))
            {
                return 0;
            }

            n = new Vector(point.x - cen.x, 0, point.z - cen.z);
            n.normalize();

            return (ray.direction.dot(n) < 0 ? 1 : -1);
        }

        /// <summary>
        /// Used for intersection testing to see if a Ray intersects the top of the ring around the Cylinder.
        /// </summary>
        /// <param name="ray"></param>
        /// <param name="cen"></param>
        /// <param name="ir"></param>
        /// <param name="or"></param>
        /// <param name="t"></param>
        /// <param name="n"></param>
        /// <returns></returns>
        private static int intersectAnnulus(Ray ray, Vector cen, double ir, double or, ref double t, ref Vector n)
        {
            Vector ori = ray.origin;
            Vector dir = ray.direction;

            t = (cen.y - ori.y) / dir.y;

            if (t < Settings.EPSILON) { return 0; }

            Vector point = ray.pointAtParameter(t);
            double dx = point.x - cen.x;
            double dz = point.z - cen.z;
            double test = Math.Sqrt((dx * dx) + (dz * dz));

            if (test < ir || test > or) { return 0; }

            n = new Vector(0, 1, 0);

            return (ray.direction.dot(n) < 0 ? 1 : -1);
        }

        /// <summary>
        /// Test if the given Ray intersects this Cylinder.
        /// </summary>
        /// <param name="ray">Ray to test for intersection.</param>
        /// <param name="hit">Hit to update with intersection info.</param>
        /// <returns>1 if the Ray intersects this Cylinder from the outside,
        /// -1 if the Ray intersects this Cylinder from the inside,
        /// 0 if no intersection occurs.</returns>
        public override int Intersect(Ray ray, Hit hit)
        {
            double ot = -1;
            Vector no = null;
            int outer = intersectFiniteCylinder(ray, center, outerRad, height, ref ot, ref no);

            double it = -1;
            Vector ni = null;
            int inner = intersectFiniteCylinder(ray, center, innerRad, height, ref it, ref ni);

            double tt = -1;
            Vector nt = null;
            Vector ct = center + new Vector(0, height / 2, 0);
            int top = intersectAnnulus(ray, ct, innerRad, outerRad, ref tt, ref nt);

            double bt = -1;
            Vector nb = null;
            Vector cb = center - new Vector(0, height / 2, 0);
            int bottom = intersectAnnulus(ray, cb, innerRad, outerRad, ref bt, ref nb);

            int ans = 0;
            if (outer != 0 && ot < hit.t)
            {
                hit.t = ot;
                hit.normal = no;
                hit.material = material;
                ans = outer;
            }

            if (inner != 0 && it < hit.t)
            {
                hit.t = it;
                hit.normal = -ni;
                hit.material = material;
                ans = -inner;
            }

            if (top != 0 && tt < hit.t)
            {
                hit.t = tt;
                hit.normal = nt;
                hit.material = material;
                ans = top;
            }

            if (bottom != 0 && bt < hit.t)
            {
                hit.t = bt;
                hit.normal = -nb;
                hit.material = material;
                ans = -bottom;
            }

            return ans;
        }

        public static Cylinder Parse(MappingEntry entry)
        {
            if (entry == null) { throw new ArgumentNullException("entry"); }

            Mapping map = (Mapping)entry.Value;
            Material material = Material.Parse(map.Find("material"));
            MappingEntry found = map.Find("center");
            if (found == null) { throw new ArgumentException("No center entry"); }
            Vector center = Vector.FromSequence((Sequence)found.Value);
            found = map.Find("height");
            if (found == null) { throw new ArgumentException("No height entry"); }
            double height = double.Parse(found.Value.ToString());
            found = map.Find("inner");
            if (found == null) { throw new ArgumentException("No inner entry"); }
            double inner = double.Parse(found.Value.ToString());
            found = map.Find("outer");
            if (found == null) { throw new ArgumentException("No outer entry"); }
            double outer = double.Parse(found.Value.ToString());

            return new Cylinder(center, height, inner, outer, ref material);
        }

        /// <summary>
        /// Get a string representation of this Cylinder.
        /// </summary>
        /// <returns>A string representing this Cylinder.</returns>
        public override string ToString()
        {
            return "Cylinder: { center: " + center + ", height: " + height + ", inner: " + innerRad + ", outer: " + outerRad + ", material: " + material + " }";
        }
    }
}
